summaryrefslogtreecommitdiff
path: root/app/[lng]/partners/(partners)
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]/partners/(partners)')
-rw-r--r--app/[lng]/partners/(partners)/pq_new/[id]/page.tsx206
-rw-r--r--app/[lng]/partners/(partners)/pq_new/page.tsx298
-rw-r--r--app/[lng]/partners/(partners)/site-visit/page.tsx30
3 files changed, 534 insertions, 0 deletions
diff --git a/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx b/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx
new file mode 100644
index 00000000..5a8313cc
--- /dev/null
+++ b/app/[lng]/partners/(partners)/pq_new/[id]/page.tsx
@@ -0,0 +1,206 @@
+import { Metadata } from "next";
+import Link from "next/link";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+import { Button } from "@/components/ui/button";
+import { ArrowLeft, LogIn } from "lucide-react";
+import { Shell } from "@/components/shell";
+import { getPQById, getPQDataByVendorId } from "@/lib/pq/service";
+import { unstable_noStore as noStore } from 'next/cache';
+import { Alert, AlertDescription } from "@/components/ui/alert";
+import { PQInputTabs } from "@/components/pq-input/pq-input-tabs";
+
+export const metadata: Metadata = {
+ title: "사전 평가 (PQ) 작성",
+ description: "사전 평가 항목을 작성합니다.",
+};
+
+// 페이지가 기본적으로 동적임을 나타냄
+export const dynamic = "force-dynamic";
+
+interface PQEditPageProps {
+ params: Promise<{ id: string }>;
+}
+
+export default async function PQEditPage(props: PQEditPageProps) {
+ // 캐시 비활성화
+ noStore();
+
+ const params = await props.params;
+ const pqSubmissionId = parseInt(params.id, 10);
+
+ // 인증 확인
+ const session = await getServerSession(authOptions);
+
+ // 로그인 확인
+ if (!session || !session.user) {
+ return (
+ <Shell className="gap-6">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 사전 평가 (PQ) 작성
+ </h2>
+ </div>
+ </div>
+
+ <div className="flex flex-col items-center justify-center py-12 text-center">
+ <div className="rounded-lg border border-dashed p-10 shadow-sm">
+ <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3>
+ <p className="mb-6 text-muted-foreground">
+ 사전 평가를 작성하려면 먼저 로그인하세요.
+ </p>
+ <Button size="lg" asChild>
+ <Link href={`/partners?callbackUrl=/partners/pq/${pqSubmissionId}`}>
+ <LogIn className="mr-2 h-4 w-4" />
+ 로그인하기
+ </Link>
+ </Button>
+ </div>
+ </div>
+ </Shell>
+ );
+ }
+
+ // 세션에서 vendorId 가져오기
+ const vendorId = session.user.companyId;
+
+ // 벤더 권한 확인
+ if (session.user.domain !== "partners" || !vendorId) {
+ return (
+ <Shell className="gap-6">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 접근 권한 없음
+ </h2>
+ </div>
+ </div>
+ <div className="flex flex-col items-center justify-center py-12 text-center">
+ <div className="rounded-lg border border-dashed p-10 shadow-sm">
+ <h3 className="mb-2 text-xl font-semibold">벤더 계정이 필요합니다</h3>
+ <p className="mb-6 text-muted-foreground">
+ 벤더 계정으로 로그인해주세요.
+ </p>
+ </div>
+ </div>
+ </Shell>
+ );
+ }
+
+ const idAsNumber = Number(vendorId);
+
+ try {
+ // PQ Submission 정보 조회 (vendorPQSubmissions 테이블에서)
+ const pqSubmission = await getPQById(pqSubmissionId, idAsNumber);
+
+ // 이 PQ가 현재 로그인한 벤더의 것인지 확인
+ if (pqSubmission.vendorId !== idAsNumber) {
+ throw new Error("Access denied - This PQ belongs to another vendor");
+ }
+
+ // PQ 데이터 조회 (pqCriterias와 답변)
+ const pqData = await getPQDataByVendorId(idAsNumber, pqSubmission.projectId || undefined);
+
+ // 상태에 따른 읽기 전용 모드 결정
+ const isReadOnly = [ "APPROVED"].includes(pqSubmission.status);
+ const statusText = pqSubmission.status === "SUBMITTED" ? "제출됨" :
+ pqSubmission.status === "APPROVED" ? "승인됨" :
+ pqSubmission.status === "REJECTED" ? "거부됨" : "작성 중";
+
+ const pageTitle = pqSubmission.type === "PROJECT"
+ ? `프로젝트 PQ - ${pqSubmission.projectName || pqSubmission.projectCode}`
+ : pqSubmission.type === "NON_INSPECTION"
+ ? "미실사 PQ"
+ : "일반 PQ";
+
+ // 프로젝트 정보 (프로젝트 PQ인 경우)
+ const projectPQ = pqSubmission.projectId ? {
+ id: pqSubmission.projectId,
+ projectId: pqSubmission.projectId,
+ projectCode: pqSubmission.projectCode || '',
+ projectName: pqSubmission.projectName || '',
+ status: pqSubmission.status,
+ submittedAt: pqSubmission.submittedAt,
+ } : null;
+
+ return (
+ <Shell className="gap-6">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center gap-4">
+ <Button variant="outline" size="sm" asChild>
+ <Link href="/partners/pq_new">
+ <ArrowLeft className="w-4 h-4 mr-2" />
+ 목록으로
+ </Link>
+ </Button>
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ {pageTitle}
+ </h2>
+ <p className="text-muted-foreground">
+ 상태: {statusText}
+ {pqSubmission.status === "REJECTED" && pqSubmission.rejectReason && (
+ <span className="text-destructive ml-2">
+ (거부 사유: {pqSubmission.rejectReason})
+ </span>
+ )}
+ </p>
+ </div>
+ </div>
+ </div>
+
+ {/* 읽기 전용 모드 알림 */}
+ {/* {isReadOnly && (
+ <Alert>
+ <AlertDescription>
+ 이 PQ는 현재 제출된 상태입니다. SHI 코멘트를 확인 후 재제출이 가능합니다.
+ </AlertDescription>
+ </Alert>
+ )} */}
+
+ {/* PQ 입력 컴포넌트 */}
+ <PQInputTabs
+ data={pqData}
+ vendorId={idAsNumber}
+ projectId={pqSubmission.projectId || undefined}
+ projectData={projectPQ}
+ isReadOnly={isReadOnly}
+ currentPQ={{ // 현재 PQ Submission 정보 전달
+ id: pqSubmission.id,
+ status: pqSubmission.status,
+ type: pqSubmission.type
+ }}
+ />
+ </Shell>
+ );
+ } catch (error) {
+ console.error("Error loading PQ:", error);
+
+ return (
+ <Shell className="gap-6">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 오류 발생
+ </h2>
+ </div>
+ </div>
+ <div className="flex flex-col items-center justify-center py-12 text-center">
+ <div className="rounded-lg border border-dashed p-10 shadow-sm">
+ <h3 className="mb-2 text-xl font-semibold">PQ를 불러올 수 없습니다</h3>
+ <p className="mb-6 text-muted-foreground">
+ 요청하신 PQ를 찾을 수 없거나 접근 권한이 없습니다.
+ </p>
+ <Button asChild>
+ <Link href="/partners/pq">
+ <ArrowLeft className="mr-2 h-4 w-4" />
+ 목록으로 돌아가기
+ </Link>
+ </Button>
+ </div>
+ </div>
+ </Shell>
+ );
+ }
+} \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/pq_new/page.tsx b/app/[lng]/partners/(partners)/pq_new/page.tsx
new file mode 100644
index 00000000..eea5b21d
--- /dev/null
+++ b/app/[lng]/partners/(partners)/pq_new/page.tsx
@@ -0,0 +1,298 @@
+import * as React from "react";
+import Link from "next/link";
+import { Metadata } from "next";
+import { getServerSession } from "next-auth/next";
+import { authOptions } from "@/app/api/auth/[...nextauth]/route";
+import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
+import { Button } from "@/components/ui/button";
+import { Badge } from "@/components/ui/badge";
+import { LogIn, Edit, Eye, Ellipsis } from "lucide-react";
+import { Shell } from "@/components/shell";
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from "@/components/ui/table";
+import { unstable_noStore as noStore } from 'next/cache';
+import { getAllPQsByVendorId, getPQStatusCounts } from "@/lib/pq/service";
+import { InformationButton } from "@/components/information/information-button";
+import {
+ DropdownMenu,
+ DropdownMenuTrigger,
+ DropdownMenuContent,
+ DropdownMenuItem,
+} from "@/components/ui/dropdown-menu";
+
+export const metadata: Metadata = {
+ title: "사전 평가 (PQ) 목록",
+ description: "요청된 사전 평가 목록을 확인하고 작성합니다.",
+};
+
+// 페이지가 기본적으로 동적임을 나타냄
+export const dynamic = "force-dynamic";
+
+function getStatusBadge(status: string) {
+ switch (status) {
+ case "REQUESTED":
+ return <Badge variant="outline">요청됨</Badge>;
+ case "IN_PROGRESS":
+ return <Badge variant="secondary">진행 중</Badge>;
+ case "SUBMITTED":
+ return <Badge variant="default">제출됨</Badge>;
+ case "APPROVED":
+ return <Badge variant="default">승인됨</Badge>;
+ case "REJECTED":
+ return <Badge variant="destructive">거부됨</Badge>;
+ default:
+ return <Badge variant="outline">{status}</Badge>;
+ }
+}
+
+function getFormattedDate(date: Date | null) {
+ if (!date) return "-";
+ return new Intl.DateTimeFormat("ko-KR", {
+ year: "numeric",
+ month: "2-digit",
+ day: "2-digit",
+ }).format(new Date(date));
+}
+
+export default async function PQListPage() {
+ // 캐시 비활성화
+ noStore();
+
+ // 인증 확인
+ const session = await getServerSession(authOptions);
+
+ // 로그인 확인
+ if (!session || !session.user) {
+ return (
+ <Shell className="gap-6">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 사전 평가 (PQ) 목록
+ </h2>
+ <p className="text-muted-foreground">
+ 요청된 사전 평가 목록을 확인하고 작성합니다.
+ </p>
+ </div>
+ </div>
+
+ <div className="flex flex-col items-center justify-center py-12 text-center">
+ <div className="rounded-lg border border-dashed p-10 shadow-sm">
+ <h3 className="mb-2 text-xl font-semibold">로그인이 필요합니다</h3>
+ <p className="mb-6 text-muted-foreground">
+ 사전 평가를 확인하려면 먼저 로그인하세요.
+ </p>
+ <Button size="lg" asChild>
+ <Link href="/partners?callbackUrl=/partners/pq">
+ <LogIn className="mr-2 h-4 w-4" />
+ 로그인하기
+ </Link>
+ </Button>
+ </div>
+ </div>
+ </Shell>
+ );
+ }
+
+ // 세션에서 vendorId 가져오기
+ const vendorId = session.user.companyId;
+
+ // 벤더 권한 확인
+ if (session.user.domain !== "partners" || !vendorId) {
+ return (
+ <Shell className="gap-6">
+ <div className="flex items-center justify-between">
+ <div>
+ <h2 className="text-2xl font-bold tracking-tight">
+ 접근 권한 없음
+ </h2>
+ </div>
+ </div>
+ <div className="flex flex-col items-center justify-center py-12 text-center">
+ <div className="rounded-lg border border-dashed p-10 shadow-sm">
+ <h3 className="mb-2 text-xl font-semibold">벤더 계정이 필요합니다</h3>
+ <p className="mb-6 text-muted-foreground">
+ 벤더 계정으로 로그인해주세요.
+ </p>
+ </div>
+ </div>
+ </Shell>
+ );
+ }
+
+ const idAsNumber = Number(vendorId);
+
+ // 데이터 가져오기 (병렬 실행)
+ const [pqList, pqStatusCounts] = await Promise.all([
+ getAllPQsByVendorId(idAsNumber),
+ getPQStatusCounts(idAsNumber),
+ ]);
+
+ return (
+ <Shell className="gap-6">
+ <div className="flex justify-between items-center">
+ <div>
+ <div className="flex items-center gap-2">
+ <h2 className="text-2xl font-bold tracking-tight">사전 평가 (PQ) 목록</h2>
+ <InformationButton pagePath="partners/pq_new" />
+ </div>
+ <p className="text-muted-foreground">
+ 요청된 사전 평가 목록을 확인하고 작성합니다.
+ </p>
+ </div>
+ </div>
+
+ {/* PQ 상태 요약 카드 */}
+ <div className="grid gap-4 md:grid-cols-4">
+ <Card>
+ <CardHeader className="py-4">
+ <CardTitle className="text-base">총 PQ</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">
+ {Object.values(pqStatusCounts).reduce((sum, count) => sum + count, 0)}건
+ </div>
+ </CardContent>
+ </Card>
+ <Card>
+ <CardHeader className="py-4">
+ <CardTitle className="text-base">작성 대기</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">
+ {(pqStatusCounts.REQUESTED || 0) + (pqStatusCounts.IN_PROGRESS || 0)}건
+ </div>
+ </CardContent>
+ </Card>
+ <Card>
+ <CardHeader className="py-4">
+ <CardTitle className="text-base">제출됨</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">
+ {pqStatusCounts.SUBMITTED || 0}건
+ </div>
+ </CardContent>
+ </Card>
+ <Card>
+ <CardHeader className="py-4">
+ <CardTitle className="text-base">승인됨</CardTitle>
+ </CardHeader>
+ <CardContent>
+ <div className="text-2xl font-bold">
+ {pqStatusCounts.APPROVED || 0}건
+ </div>
+ </CardContent>
+ </Card>
+ </div>
+
+ {/* PQ 목록 테이블 */}
+ <Card>
+ <CardHeader>
+ <CardTitle>PQ 목록</CardTitle>
+ </CardHeader>
+ <CardContent className="p-0">
+ <Table>
+ <TableHeader>
+ <TableRow>
+ <TableHead>유형</TableHead>
+ <TableHead>PQ 번호</TableHead>
+ <TableHead>프로젝트</TableHead>
+ <TableHead>상태</TableHead>
+ <TableHead>요청일</TableHead>
+ <TableHead>제출일</TableHead>
+ <TableHead>승인일</TableHead>
+ <TableHead>액션</TableHead>
+ </TableRow>
+ </TableHeader>
+ <TableBody>
+ {pqList.length === 0 ? (
+ <TableRow>
+ <TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
+ 요청된 PQ가 없습니다.
+ </TableCell>
+ </TableRow>
+ ) : (
+ pqList.map((pq) => {
+ const canEdit = ["REQUESTED", "IN_PROGRESS", "REJECTED"].includes(pq.status);
+ const canView = ["SUBMITTED", "APPROVED"].includes(pq.status);
+
+ return (
+ <TableRow key={pq.id}>
+ <TableCell>
+ <Badge variant={
+ pq.type === "PROJECT" ? "default" :
+ pq.type === "NON_INSPECTION" ? "secondary" :
+ "outline"
+ }>
+ {pq.type === "PROJECT" ? "프로젝트" :
+ pq.type === "NON_INSPECTION" ? "미실사" :
+ "일반"}
+ </Badge>
+ </TableCell>
+ <TableCell>
+ {pq.pqNumber || "-"}
+ </TableCell>
+ <TableCell>
+ {pq.projectName || "-"}
+ </TableCell>
+ <TableCell>
+ {getStatusBadge(pq.status)}
+ </TableCell>
+ <TableCell>
+ {getFormattedDate(pq.createdAt)}
+ </TableCell>
+ <TableCell>
+ {getFormattedDate(pq.submittedAt)}
+ </TableCell>
+ <TableCell>
+ {getFormattedDate(pq.approvedAt)}
+ </TableCell>
+ <TableCell>
+ <DropdownMenu>
+ <DropdownMenuTrigger asChild>
+ <Button
+ aria-label="액션 메뉴 열기"
+ variant="ghost"
+ className="flex size-8 p-0 data-[state=open]:bg-muted"
+ >
+ <Ellipsis className="size-4" aria-hidden="true" />
+ </Button>
+ </DropdownMenuTrigger>
+ <DropdownMenuContent align="end" className="w-36">
+ {canEdit && (
+ <DropdownMenuItem asChild>
+ <Link href={`/partners/pq_new/${pq.id}`}>
+ <Edit className="mr-2 h-4 w-4" />
+ 작성
+ </Link>
+ </DropdownMenuItem>
+ )}
+ {canView && (
+ <DropdownMenuItem asChild>
+ <Link href={`/partners/pq_new/${pq.id}`}>
+ <Eye className="mr-2 h-4 w-4" />
+ 보기
+ </Link>
+ </DropdownMenuItem>
+ )}
+ </DropdownMenuContent>
+ </DropdownMenu>
+ </TableCell>
+ </TableRow>
+ );
+ })
+ )}
+ </TableBody>
+ </Table>
+ </CardContent>
+ </Card>
+ </Shell>
+ );
+} \ No newline at end of file
diff --git a/app/[lng]/partners/(partners)/site-visit/page.tsx b/app/[lng]/partners/(partners)/site-visit/page.tsx
new file mode 100644
index 00000000..92580b35
--- /dev/null
+++ b/app/[lng]/partners/(partners)/site-visit/page.tsx
@@ -0,0 +1,30 @@
+import { getServerSession } from "next-auth"
+import { authOptions } from "@/app/api/auth/[...nextauth]/route"
+import { getSiteVisitRequestsByVendorId } from "@/lib/site-visit/service"
+import { ClientSiteVisitWrapper } from "@/lib/site-visit/client-site-visit-wrapper"
+import { unstable_noStore as noStore } from 'next/cache'
+
+// 페이지가 기본적으로 동적임을 나타냄
+export const dynamic = "force-dynamic"
+
+export default async function SiteVisitPage() {
+ // Opt out of caching for this route
+ noStore()
+
+ // 세션
+ const session = await getServerSession(authOptions)
+ // 세션에서 vendorId 가져오기
+ const vendorId = session?.user.companyId
+ const idAsNumber = Number(vendorId)
+
+ // 방문실사 요청 목록 가져오기
+ const siteVisitRequests = await getSiteVisitRequestsByVendorId(idAsNumber)
+
+ // 클라이언트 컴포넌트로 데이터 전달
+ return (
+ <ClientSiteVisitWrapper
+ siteVisitRequests={siteVisitRequests}
+ vendorId={idAsNumber}
+ />
+ )
+} \ No newline at end of file